﻿using System;
using System.Text;
using System.Net;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;

namespace Touchality.FoursquareApi
{
    [global::System.Serializable]
    public class FoursquareServiceException : Exception
    {
        public FoursquareServiceException() { }
        public FoursquareServiceException(string message) : base(message) { }
        public FoursquareServiceException(string message, Exception inner) : base(message, inner) { }
    }

    public class ListCompletedEventArgs<T>
    {
        public bool Cancelled { get; set; }
        public Exception Error { get; set; }
        public IList<T> Result { get; set; }
    }

    public class ItemCompletedEventArgs<T>
    {
        public bool Cancelled { get; set; }
        public Exception Error { get; set; }
        public T Result { get; set; }
    }

    public class GetVenuesCompletedEventArgs : ListCompletedEventArgs<Venue> { }
    public class GetCheckInsCompletedEventArgs : ListCompletedEventArgs<CheckIn> { }
    public class GetUserCompletedEventArgs : ItemCompletedEventArgs<User> { }
    public class GetVenueCompletedEventArgs : ItemCompletedEventArgs<Venue> { }
    public class CheckInCompletedEventArgs : ItemCompletedEventArgs<CheckIn> { }
    public class GetTipsCompletedEventArgs : ListCompletedEventArgs<Tip> { }

    public static class Foursquare
    {


        #region Helper Methods

        private static string GetAuthHeader()
        {
            return authHeader;
        }

        private static string authHeader = string.Empty;
        public static void SetCredentials(string username, string password)
        {
            var base64Creds = Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format("{0}:{1}", username, password)));
            authHeader = "Basic " + base64Creds;
        }

        public static WebClient GetWebClient(bool authenicated)
        {
            var webClient = new WebClient();
            if (authenicated)
            {
                webClient.Headers["Authorization"] = GetAuthHeader();
            }
            return webClient;
        }

        public static double ToDouble(string value, double dflt)
        {
            double result = double.NaN;
            if (!double.TryParse(value, out result))
            {
                return dflt;
            }
            return result;
        }

        #endregion

        #region SignIn
        public static void SignIn(GetUserCallback callback, string username, string password)
        {
            SetCredentials(username, password);
            GetUser(callback);
        }
        #endregion

        #region GetVenues
        public delegate void GetVenuesCallback(GetVenuesCompletedEventArgs e);


        public static void GetVenues(GetVenuesCallback callback, double geolat, double geolong)
        {
            GetVenues(callback, null, geolat, geolong, 1);
        }

        public static void GetVenues(GetVenuesCallback callback, double geolat, double geolong, int sortOrder)
        {
            GetVenues(callback, null, geolat, geolong, sortOrder);
        }


        public static void GetVenues(GetVenuesCallback callback, string query, double geolat, double geolong)
        {
            GetVenues(callback, query, geolat, geolong, 1);
        }

        public static void GetVenues(GetVenuesCallback callback, string query, double geolat, double geolong, int sortOrder)
        {
            string url;
            if (string.IsNullOrEmpty(query))
            {
                url = string.Format("http://api.foursquare.com/v1/venues?geolat={0}&geolong={1}&l=50", geolat, geolong);
            }
            else
            {
                query = Uri.EscapeUriString(query);
                url = string.Format("http://api.foursquare.com/v1/venues?geolat={0}&geolong={1}&l=50&q=", geolat, geolong, query);
            }

            var webClient = GetWebClient(true);

            webClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(
                delegate(object sender, DownloadStringCompletedEventArgs e)
                {
                    GetVenuesCallback cb = e.UserState as GetVenuesCallback;
                    if (e.Error == null)
                    {
                        try
                        {
                            var venues = ParseGetVenues(e.Result);
                            cb(new GetVenuesCompletedEventArgs
                            {
                                Cancelled = e.Cancelled,
                                Error = e.Error,
                                Result = venues,
                            });
                        }
                        catch (Exception ex)
                        {
                            throw ex;
                        }
                        Debug.WriteLine(e.Result);
                    }
                    else
                    {
                        cb(new GetVenuesCompletedEventArgs
                        {
                            Cancelled = e.Cancelled,
                            Error = e.Error,
                            Result = null,
                        });
                        Debug.WriteLine(e.Error);
                    }
                });

            webClient.DownloadStringAsync(new Uri(url), callback);
        }

        //parses the foursquare response to create a list of venues sorted by distance. 
        private static IList<Venue> ParseGetVenues(string xml)
        {
            try
            {
                var dom = XDocument.Parse(xml);

                var items = from item in dom.Descendants("venue")
                            select new Venue
                            {
                                id = (string)item.Element("id"),
                                phone = (string)item.Element("phone"),
                                name = (string)item.Element("name"),
                                address = string.IsNullOrEmpty((string)item.Element("address")) ? "" : (string)item.Element("address"),
                                crossstreet = (string)item.Element("crossstreet"),
                                geolat = (string)item.Element("geolat"),
                                geolong = (string)item.Element("geolong"),
                                distance = (string)item.Element("distance"),
                                city = (string)item.Element("city"),
                                state = (string)item.Element("state"),
                                zip = (string)item.Element("zip"),
                                twitter = (string)item.Element("twitter"),
                                tips = string.IsNullOrEmpty((string)item.Element("tips")) ? null : ParseGetTips((string)item.Element("tips")),
                                specials = string.IsNullOrEmpty((string)item.Element("specials")) ? null : ParseGetSpecials((string)item.Element("specials")),
                            };

                return items.ToList<Venue>();
            }
            catch
            {
                return new List<Venue>();
            }
        }

    

        // These are only returned as part of the venue details
        private static List<Special> ParseGetSpecials(string xml)
        {
            return null;

            /*
             * 
             *         
             * specials:{
            special:{
                id:2,
                type:'mayor', /count/frequency/other
                kind:'nearby', /here
                message:'If you\'re the mayor, show the bartender and your first drink is free! (just beer and well drinks guys, let\'s be fair)',
               [ venue:{
                    id:333
                }] \\appears only for kind:nearby
            }
        },

             * 
             */
        }

        #endregion

        #region GetCheckIns

        public delegate void GetCheckInsCallback(GetCheckInsCompletedEventArgs e);

        public static void GetCheckIns(GetCheckInsCallback callback, double geolat, double geolong)
        {
            var url = string.Format("http://api.foursquare.com/v1/checkins?geolat={0}&geolong={1}", geolat, geolong);

            var webClient = GetWebClient(true);

            webClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(
                delegate(object sender, DownloadStringCompletedEventArgs e)
                {
                    GetCheckInsCallback cb = e.UserState as GetCheckInsCallback;
                    if (e.Error == null)
                    {
                        try
                        {
                            var checkIns = ParseGetCheckIns(e.Result);
                            cb(new GetCheckInsCompletedEventArgs
                            {
                                Cancelled = e.Cancelled,
                                Error = e.Error,
                                Result = checkIns,
                            });
                        }
                        catch (Exception ex)
                        {
                            throw ex;
                        }
                        Debug.WriteLine(e.Result);
                    }
                    else
                    {
                        cb(new GetCheckInsCompletedEventArgs
                        {
                            Cancelled = e.Cancelled,
                            Error = e.Error,
                            Result = null,
                        });
                        Debug.WriteLine(e.Error);
                    }
                });

            webClient.DownloadStringAsync(new Uri(url), callback);
        }

        private static IList<CheckIn> ParseGetCheckIns(string xml)
        {
            try
            {
                var dom = XDocument.Parse(xml);

                var items = from item in dom.Descendants("checkin")
                            select new CheckIn
                            {
                                id = (string)item.Element("id"),
                                created = (string)item.Element("created"),
                                timezone = (string)item.Element("timezone"),
                                ismayor = (string)item.Element("ismayor"),
                                display = (string)item.Element("display"),
                                distance = ToDouble(item.Element("distance").Value, double.NaN),
                                user = new User
                                {
                                    id = (string)item.Element("user").Element("id"),
                                    photo = (string)item.Element("user").Element("photo"),
                                    firstname = (string)item.Element("user").Element("firstname"),
                                    lastname = (string)item.Element("user").Element("lastname")
                                },
                                venue = new Venue
                                {
                                    name = item.Element("venue") != null ? (string)item.Element("venue").Element("name") : "Off the Grid",
                                    address = item.Element("venue") != null ? (string)item.Element("venue").Element("address") : string.Empty,
                                    geolat = item.Element("venue") != null ? (string)item.Element("venue").Element("geolat") : string.Empty,
                                    geolong = item.Element("venue") != null ? (string)item.Element("venue").Element("geolong") : string.Empty,
                                }
                            };

                return items.ToList<CheckIn>();

            }
            catch (Exception)
            {
                return new List<CheckIn>();
            }
        }

        //venue = new Venue
        //{
        //    id = (string)item.Element("venue").Element("id"),
        //    name = (string)item.Element("venue").Element("name") ?? "Off the Grid",
        //    address = (string)item.Element("venue").Element("address"),
        //    crossstreet = (string)item.Element("venue").Element("crossstreet"),
        //    geolat = (string)item.Element("venue").Element("geolat"),
        //    geolong = (string)item.Element("venue").Element("geolong"),
        //    distance = (string)item.Element("venue").Element("distance"),
        //    state = (string)item.Element("venue").Element("state"),
        //    zip = (string)item.Element("venue").Element("zip"),
        //    phone = (string)item.Element("venue").Element("phone"),
        //    twitter = (string)item.Element("venue").Element("twitter"),
        //}

        #endregion

        #region Get User

        public delegate void GetUserCallback(GetUserCompletedEventArgs e);

        public static void GetUser(GetUserCallback callback)
        {
            GetUser(callback, null);
        }

        public static void GetUser(GetUserCallback callback, string id)
        {
            string url;
            if (id == null)
            {
                url = "http://api.foursquare.com/v1/user?badges=1&mayor=1";
            }
            else
            {
                url = string.Format("http://api.foursquare.com/v1/user?uid={0}&badges=1&mayor=1", id);
            }

            var webClient = GetWebClient(true);

            webClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(
                delegate(object sender, DownloadStringCompletedEventArgs e)
                {
                    GetUserCallback cb = e.UserState as GetUserCallback;
                    if (e.Error == null)
                    {
                        try
                        {
                            //<error>Foursquare is over capacity, please try again in a few minutes!</error>
                            if (e.Result.StartsWith("<error>"))
                            {
                                cb(new GetUserCompletedEventArgs
                                {
                                    Cancelled = e.Cancelled,
                                    Error = new FoursquareServiceException(e.Result.Replace("<error>", string.Empty).Replace("</error>", string.Empty)),
                                    Result = null,
                                });
                                return;
                            }

                            var result = ParseGetUser(e.Result);
                            cb(new GetUserCompletedEventArgs
                            {
                                Cancelled = e.Cancelled,
                                Error = e.Error,
                                Result = result,
                            });
                        }
                        catch (Exception ex)
                        {
                            throw ex;
                        }
                        Debug.WriteLine(e.Result);
                    }
                    else
                    {
                        cb(new GetUserCompletedEventArgs
                        {
                            Cancelled = e.Cancelled,
                            Error = e.Error,
                            Result = null,
                        });
                        Debug.WriteLine(e.Error);
                    }
                });

            webClient.DownloadStringAsync(new Uri(url), callback);
        }

        private static User ParseGetUser(string xml)
        {
            try
            {
                var dom = XDocument.Parse(xml);

                var items = from item in dom.Descendants("user")
                            select new User
                            {
                                id = (string)item.Element("id"),
                                firstname = (string)item.Element("firstname"),
                                lastname = (string)item.Element("lastname"),
                                email = (string)item.Element("email"),
                                friendstatus = (string)item.Element("friendstatus"),
                                gender = (string)item.Element("gender"),
                                phone = (string)item.Element("phone"),
                                photo = (string)item.Element("photo"),
                            };


                var user = items.FirstOrDefault<User>();

                var badges = from item in dom.Descendants("user").Descendants("badge")
                             select new Badge
                             {
                                 id = (string)item.Element("id"),
                                 name = (string)item.Element("name"),
                                 icon = (string)item.Element("icon"),
                                 description = (string)item.Element("description"),
                             };

                if (badges.Count() > 0)
                    user.badges = badges.ToList<Badge>();
                else
                    user.badges = new List<Badge>();

                var checkIn = from item in dom.Descendants("user").Descendants("checkin")
                              select new CheckIn
                              {
                                  id = (string)item.Element("id"),
                                  created = (string)item.Element("created"),
                                  timezone = (string)item.Element("timezone"),
                                  shout = (string)item.Element("shout"),
                                  display = (string)item.Element("display"),
                              };

                user.checkin = checkIn.FirstOrDefault<CheckIn>();

                try
                {
                    var q = from item in dom.Descendants("user").Descendants("checkin").Descendants("venue")
                            select new Venue
                            {
                                id = (string)item.Element("id"),
                                name = (string)item.Element("name"),
                                address = (string)item.Element("address"),
                                city = (string)item.Element("city"),
                                state = (string)item.Element("state"),
                                zip = (string)item.Element("zip"),
                                geolat = (string)item.Element("geolat"),
                                geolong = (string)item.Element("geolong"),
                            };
                    user.venue = q.FirstOrDefault<Venue>();
                }
                catch
                {
                }


                return user;

            }
            catch (Exception)
            {
                return null;
            }
        }

        #endregion

        #region Get Venue

        public delegate void GetVenueCallback(GetVenueCompletedEventArgs e);

        public static void GetVenue(GetVenueCallback callback, string vid)
        {
            string url = string.Format("http://api.foursquare.com/v1/venue?vid={0}", vid);

            var webClient = GetWebClient(true);

            webClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(
                delegate(object sender, DownloadStringCompletedEventArgs e)
                {
                    GetVenueCallback cb = e.UserState as GetVenueCallback;
                    if (e.Error == null)
                    {
                        try
                        {
                            var result = ParseGetVenue(e.Result);
                            cb(new GetVenueCompletedEventArgs
                            {
                                Cancelled = e.Cancelled,
                                Error = e.Error,
                                Result = result,
                            });
                        }
                        catch (Exception ex)
                        {
                            throw ex;
                        }
                        Debug.WriteLine(e.Result);
                    }
                    else
                    {
                        cb(new GetVenueCompletedEventArgs
                        {
                            Cancelled = e.Cancelled,
                            Error = e.Error,
                            Result = null,
                        });
                        Debug.WriteLine(e.Error);
                    }
                });

            webClient.DownloadStringAsync(new Uri(url), callback);
        }

        private static Venue ParseGetVenue(string xml)
        {
            try
            {
                var dom = XDocument.Parse(xml);

                var items = from item in dom.Descendants("venue")
                            select new Venue
                            {
                                id = (string)item.Element("id"),
                                phone = (string)item.Element("phone"),
                                name = (string)item.Element("name"),
                                address = (string)item.Element("address"),
                                crossstreet = (string)item.Element("crossstreet"),
                                geolat = (string)item.Element("geolat"),
                                geolong = (string)item.Element("geolong"),
                                distance = (string)item.Element("distance"),
                                city = (string)item.Element("city"),
                                state = (string)item.Element("state"),
                                zip = (string)item.Element("zip"),
                                twitter = (string)item.Element("twitter"),
                            };

                return items.FirstOrDefault<Venue>();

            }
            catch (Exception)
            {
                return null;
            }
        }

        #endregion
        
        #region Get Tips



        public delegate void GetTipsCallback(GetTipsCompletedEventArgs e);

        /// <summary>
        /// Calls the foursquare Tips API. 
        /// Displays a list of tips that have been left about nearby venues by other users. requires GetVenues to be called first
        /// Future work: Can be extended to get tips from friends only, tips near someone else
        /// or sorted by popular/recent instead of distance. 
        /// </summary>    
        /// geolat - latitude (required)
        /// geolong - longitude (required)
        /// filter - one of "nearby" or "friends" indicating whether to return all nearby tips, or just nearby tips from friends (required, unless uid is specified)
        /// uid - a particular user id to get tips from (optional, see filter)
        /// sort - one of "nearby", "recent", or "popular" (optional, only applies if uid is specified, for which it is required)
        /// l - limit of results (optional, default 30)
        /// ooh, I could do a pivot on the sort orders! :)
        public static void GetTips(GetTipsCallback callback, double geoLat, double geoLong)
        {

            string filter = "nearby";
            string url = string.Format("http://api.foursquare.com/v1/tips?geolat={0}&geolong={1}&filter={2}", geoLat, geoLong, filter);

            var webClient = GetWebClient(true);

            webClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(
                delegate(object sender, DownloadStringCompletedEventArgs e)
                {
                    GetTipsCallback cb = e.UserState as GetTipsCallback;
                    if (e.Error == null)
                    {
                        try
                        {
                            var result = ParseGetTips(e.Result);
                            cb(new GetTipsCompletedEventArgs
                            {
                                Cancelled = e.Cancelled,
                                Error = e.Error,
                                Result = result,
                            });
                        }
                        catch (Exception ex)
                        {
                            throw ex;
                        }
                        Debug.WriteLine(e.Result);
                    }
                    else
                    {
                        cb(new GetTipsCompletedEventArgs
                        {
                            Cancelled = e.Cancelled,
                            Error = e.Error,
                            Result = null,
                        });
                        Debug.WriteLine(e.Error);
                    }
                });

            webClient.DownloadStringAsync(new Uri(url), callback);
        }

        //Tips can be gotten by calling for them specifically, or as details about a venue. If they were given with a venue, there will not be a venue or distance node.
        private static List<Tip> ParseGetTips(string xml)
        {
            return null;

            /* tips element if returned as part of a getvenues call
                    "tips": [
            {
                "id": 14506,
                "text": "if you stay for a while the owner is more than likely to drop by with free food or drink",
                [distance:0,]
                "created": "Fri, 26 Jun 09 18:45:49 +0000",
                "user": {
                    "id": 16970,
                    "firstname": "Greg",
                    "lastname": "S.",
                    "photo": "http://playfoursquare.s3.amazonaws.com/userpix_thumbs/4a4262911f7cf.jpg",
                    "gender": "male"
                }
                [venue:{
                    id:36235,
                    name:'KK Cafe',
                    address:'252 Divisadero st',
                    crossstreet:'Haight'
                }]
                
            },
        ],
             */



        }
        

        #endregion

        #region CheckIn

        public delegate void CheckInCallback(CheckInCompletedEventArgs e);
        
        /// <summary>
        ///  Allows you to check-in to a place.
        /// </summary>
        /// <param name="callback">receives results of the call</param>
        /// <param name="vid">(optional) ID of the venue where you want to check-in</param>
        /// <param name="venue">(optional) not necessary if you are 'shouting' or have a vid) if you don't have a venue ID or would rather prefer a 'venueless' checkin, pass the venue name as a string using this parameter. it will become an 'orphan' (no address or venueid but with geolat, geolong)</param>
        /// <param name="shout">(optional) a message about your check-in. the maximum length of this field is 140 characters</param>
        /// <param name="isPrivate">don't show your friends OR show everyone</param>
        /// <param name="sendToTwitter">send to Twitter OR don't send to Twitter</param>
        /// <param name="sendToFacebook">send to Facebook OR don't send to Facebook</param>
        /// <param name="geoLat">(optional, but recommended) latitude</param>
        /// <param name="getLong">(optional, but recommended) longitude</param>
        public static void CheckIn(CheckInCallback callback, string vid, string venue, string shout, bool isPrivate, bool sendToTwitter, bool sendToFacebook, double geoLat, double getLong)
        {

            string url = "http://api.foursquare.com/v1/checkin";

            shout = Uri.EscapeDataString(shout ?? string.Empty);
            venue = Uri.EscapeDataString(venue ?? string.Empty);


            var data = string.Format("vid={0}&venue={1}&shout={2}&private={3}&twitter={4}&facebook={5}&geolat={6}&geolong{7}", vid, venue, shout, isPrivate?1:0, sendToTwitter?1:0, sendToFacebook?1:0, geoLat, getLong);

            var webClient = GetWebClient(true);
            webClient.Headers["Content-Type"] = "application/x-www-form-urlencoded";
            webClient.UploadStringCompleted += new UploadStringCompletedEventHandler(
                delegate(object sender, UploadStringCompletedEventArgs e)
                {
                    CheckInCallback cb = e.UserState as CheckInCallback;
                    if (e.Error == null)
                    {
                        try
                        {
                            var checkIns = ParseCheckIn(e.Result);
                            cb(new CheckInCompletedEventArgs
                            {
                                Cancelled = e.Cancelled,
                                Error = e.Error,
                                Result = checkIns,
                            });
                        }
                        catch (Exception ex)
                        {
                            throw ex;
                        }
                        Debug.WriteLine(e.Result);
                    }
                    else
                    {
                        cb(new CheckInCompletedEventArgs
                        {
                            Cancelled = e.Cancelled,
                            Error = e.Error,
                            Result = null,
                        });
                        Debug.WriteLine(e.Error);
                    }
                });

            webClient.UploadStringAsync(new Uri(url), "POST", data, callback);
        }

        private static CheckIn ParseCheckIn(string xml)
        {
            try
            {
                var dom = XDocument.Parse(xml);

                var items = from item in dom.Descendants("checkin")
                            select new CheckIn
                            {
                                id = (string)item.Element("id"),
                                message = (string)item.Element("message"),
                                created = (string)item.Element("created"),
                            };

                return items.FirstOrDefault<CheckIn>();

            }
            catch (Exception)
            {
                //is it a return from a shout checkin?
                return null;

            }
        }

        #endregion
    }
}